4. The Transaction Management Service
.NET 2.0 introduces an innovative transaction management service in the System.Transactions namespace. A transaction managed by System.Transactions is stored in the thread local storage and is called the ambient transaction. System.Transactions-enabled
resource managers (such as SQL Server 2005) detect the ambient
transaction and automatically enlist in the transaction, similar to the
auto-enlistment of Enterprise Services resource managers. This for the
most part eliminates the need to manage the transaction yourself. The
main feature of System.Transactions is its ability to automatically promote the transaction across transaction managers, from the lightweight transaction manager (LTM) used with a single object and a single durable resource to the OleTx transaction manager used to manage a distributed transaction. For more information about System.Transactions, see my whitepaper "Introducing System.Transactions in the Microsoft .NET Framework Version 2" (MSDN, April 2005).
When not using Enterprise Services (and until the release of Indigo), System.Transactions supports only an explicit programming model. You typically interact with an object of type TransactionScope, defined as:
public class TransactionScope : IDisposable
{
public TransactionScope( );
public TransactionScope(TransactionScopeOption scopeOptions);
//Additional constructors
public void Complete( );
public void Dispose( );
}
As the name implies, the TransactionScope class is used to scope a code section with a transaction, as demonstrated in Example 11-6. Internally in its constructor, the TransactionScope object creates a transaction (an LTM transaction, by default) and assigns it as the ambient transaction. TransactionScope is a disposable object—the transaction will end once the Dispose( ) method is called (the end of the using statement in Example 5).
Example 5. Using the TransactionScope class
TransactionScope scope = new TransactionScope( ); using(scope) { /* Perform transactional work here */
//No errors - commit transaction scope.Complete( ); }
|
The TransactionScope object has no way of knowing whether the transaction should commit or abort. To address this, TransactionScope internally maintains a consistency bit, which is set by default to false. You can set the consistency bit to true by calling the Complete( ) method. If the consistency bit is set to false when the transaction ends, the transaction will abort; otherwise, it will try to commit. Note that once you call Complete( ), there is no way to set the consistency bit back to false.
4.1. Transaction flow management
Transaction scopes can nest both directly and indirectly. A direct scope nesting is simply one scope nested inside another. An indirect scope nesting occurs when you call a method that uses a TransactionScope
object from within a method that uses its own scope. You can also have
multiple scope nesting, involving both direct and indirect nesting. The
topmost scope is referred to as the root scope.
The question is, of course, what is the relation between the root scope
and all the nested scopes? How will nesting a scope affect the ambient
transaction? To address these questions, the TransactionScope class provides several overloaded constructors that accept an enum of the type TransactionScopeOption, defined as:
public enum TransactionScopeOption
{
Required,
RequiresNew,
Suppress
}
The value of TransactionScopeOption lets you control whether the scope takes part in a transaction, and, if so, whether it will join the ambient transaction or be the root scope of a new transaction. For example, here is how you specify the value of the TransactionScopeOption in the scope's constructor:
TransactionScope scope;
scope = new TransactionScope(TransactionScopeOption.Required);
using(scope)
{...}
The default value for the scope option is TransactionScopeOption.Required. The TransactionScope
object determines which transaction to belong to when it is
constructed. Once determined, the scope will always belong to that
transaction. TransactionScope bases its decision on two factors: whether an ambient transaction is present and the value of the TransactionScopeOption parameter.
A TransactionScope object has three options:
Join the ambient transaction.
Be
a new scope root; that is, start a new transaction and have that
transaction be the new ambient transaction inside its own scope.
Not take part in a transaction at all.
If the scope is configured with TransactionScopeOption.Suppress, it will never be part of a transaction, regardless of whether an ambient transaction is present.
If the scope is configured with TransactionScopeOption.Required,
and an ambient transaction is present, the scope will join that
transaction. If, on the other hand, there is no ambient transaction, the
scope will create a new transaction and become the root scope.
If the scope is configured with TransactionScopeOption.RequiresNew,
it will always be the root scope. It will start a new transaction, and
its transaction will be the new ambient transaction inside the scope.
The way the values of TransactionScopeOption affect the flow of the transaction is analogous to the way the integer constants provided to the Synchronization attribute control the flow of the synchronization domain. |
|
4.2. Declarative transaction support
You can use context-bound objects and call interception to provide declarative support for System.Transactions. You will need to install a server context sink that wraps the call to the next sink down the chain in a TransactionScope. Example 6 shows the TransactionAttribute
class. Obviously, you will also need a context attribute that adds a
context property that installs the sink. These two classes (TransactionAttribute and TransactionalProperty, respectively) are very similar to LogbookAttribute and LogContextProperty. The TransactionAttribute's constructor accepts an enum of the type TransactionScopeOption, indicating how the transaction should flow through this context-bound object. The default constructor uses TransactionScopeOption.Required.
Example 6. The TransactionAttribute class
using System.Transactions;
[AttributeUsage(AttributeTargets.Class)] public class TransactionAttribute : ContextAttribute { TransactionScopeOption m_TransactionOption;
public TransactionAttribute( ) : this(TransactionScopeOption.Required) {}
public TransactionAttribute(TransactionScopeOption transactionOption) : base("TransactionAttribute") { m_TransactionOption = transactionOption; } //Add a new transaction property to the new context public override void GetPropertiesForNewContext(IConstructionCallMessage ctor) { IContextProperty transactional; transactional = new TransactionalProperty(m_TransactionOption); ctor.ContextProperties.Add(transactional); } //Provides a private context public override bool IsContextOK(Context ctx, IConstructionCallMessage ctorMsg) { return false; } }
|
The TransactionalProperty class installs the TransactionSink class as a server context sink, providing it with the transaction scope option:
public class TransactionalProperty: IContextProperty,
IContributeServerContextSink
{
TransactionScopeOption m_TransactionOption;
public IMessageSink GetServerContextSink(IMessageSink nextSink)
{
IMessageSink transactionSink;
transactionSink = new TransactionSink(nextSink,m_TransactionOption);
return transactionSink;
}
//Rest of the implementation
}
The interesting work, of course, is done by TransactionSink, shown in Example 7.
Example 7. The TransactionSink class provides a transactional context
public class TransactionSink : IMessageSink { IMessageSink m_NextSink; TransactionScopeOption m_TransactionOption;
public TransactionSink(IMessageSink nextSink, TransactionScopeOption transactionOption) { m_TransactionOption = transactionOption; m_NextSink = nextSink; }
public IMessageSink NextSink { get { return m_NextSink; } } public IMessage SyncProcessMessage(IMessage msg) { IMethodReturnMessage returnedMessage = null;
Exception exception; TransactionScope scope = new TransactionScope(m_TransactionOption); using(scope) { try { returnedMessage = (IMethodReturnMessage)m_NextSink. SyncProcessMessage(msg); exception = returnedMessage.Exception; } catch(Exception sinkException) { exception = sinkException; } if(exception == null) { scope.Complete( ); } return returnedMessage; } } public IMessageCtrl AsyncProcessMessage(IMessage msg,IMessageSink replySink) { string message = "Transactional calls must be synchronous" throw new InvalidOperationException(message); } }
|
In SyncProcessMessage( ), TransactionSink constructs a new TransactionScope object, passing its constructor the original value of the TransactionScopeOption enum passed to the Transaction attribute. A using statement with the scope object wraps the call to SyncProcessMessage( ) on the next sink down the chain. If no exception has occurred, SyncProcessMessage( ) calls Complete( ) on the scope and returns.
Example 8 demonstrates the use of the Transaction attribute.
Example 8. Using the Transaction attribute
[Transaction] public class RootClass : ContextBoundObject { public void CreateObjects( ) { Class1 obj1 = new Class1( ); Class2 obj2 = new Class2( ); Class3 obj3 = new Class3( ); } }
[Transaction] public class Class1 : ContextBoundObject {}
[Transaction(TransactionScopeOption.Suppress)] public class Class2 : ContextBoundObject {}
[Transaction(TransactionScopeOption.RequiresNew)] public class Class3 : ContextBoundObject {}
|
Figure 4 depicts the resulting transactions after executing this TransactionDemo( ) method:
//Non-transactional client
class MyClient
{
public void TransactionDemo( )
{
RootClass root = new RootClass( );
root.CreateObjects( );
}
}
The RootClass class is configured to require a transaction. Since it is being called by a non-transactional client (MyClient is not even context-bound), there is no ambient transaction. RootClass therefore starts a new transaction and becomes its root. When the client calls CreateObjects( ) on the RootClass object, the object creates three other context-bound objects, each configured with a different TransactionScopeOption value. Class1 is configured to require a transaction, so it will join the transaction of the RootClass object. Class2 suppresses any transaction flow, so it will execute without an ambient transaction. Class3 requires a new transaction, so it will be placed in a new transaction.
You can add granularity to the Transaction
attribute and, instead of being object-based (that is, all calls are
transactional), make it method-based. Define a method-level attribute
and apply it to the methods you wish to be called transitionally. Have SyncProcessMessage( ) reflect the target method to see if it has the attribute, and if so, wrap the method call with a TransactionScope.